﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using XMLSchemaExtractor.Generator.DTDEntities;

namespace XMLSchemaExtractor.Generator
{
    /// <summary>
    /// DTDGenerator at http://saxon.sourceforge.net/dtdgen.html was used as reference.
    /// </summary>
    public class DTD : IGenerator
    {  
        protected internal static int MAX_ID_VALUES = 100000; //Max number of attribute values to be saved while checking for uniqueness

        private SortedDictionary<string, ElementDetails> elementList;
        private List<string> orderedElementNames;
        private Stack<StackEntry> elementStack;
        private string rootName;
        private List<string> messages;

        private int minEnumInstances = 10; //Min number of counts of an attribute for it to be considered as a candidate for an enumeration type
        private int maxEnumValues = 20; //Max number of distinct attribute values in an enumeration
        private int minEnumRatio = 3; //An attribute will be determined as an enumeration attribute only if number of instances divided by number of distinct values is more than this ratio
        private int minFixed = 5; //Min number of attributes that must appear with the same Value each time, for the Value to be considered FIXED
        private int minIdValues = 10; //Min number of attribute values that must appear for the attribute to be regarded as an ID Value

        public DTD(string filePath, int minEnumInstances, int maxEnumValues, int minEnumRatio, int minFixed, int minIdValues) : this(filePath)
        {
            this.minEnumInstances = minEnumInstances;
            this.maxEnumValues = maxEnumValues;
            this.minEnumRatio = minEnumRatio;
            this.minFixed = minFixed;
            this.minIdValues = minIdValues;
            this.messages = new List<string>();
        }

        public DTD(string filePath)
        {
            elementList = new SortedDictionary<string, ElementDetails>();
            orderedElementNames = new List<string>();
            elementStack = new Stack<StackEntry>();
            Parse(filePath);
        }

        #region Public methods to use DTD as object

        /// <summary>
        /// Gets the list of elements as they appear in the XML
        /// </summary>
        /// <returns>A list of element names (String)</returns>
        public List<string> GetOrderedElementNames() 
        {
            return orderedElementNames;
        }

        /// <summary>
        /// Gets a SortedDictionary containing with the name of each element as the key
        /// and the contents of each element as the Value
        /// </summary>
        /// <returns>A SortedDictionary with the element names as keys and their contents as values</returns>
        public SortedDictionary<string, ElementDetails> GetElements()
        {
            return elementList;
        }

        /// <summary>
        /// Gets the root name
        /// </summary>
        /// <returns>Root name as String</returns>
        public string GetRootElementName()
        {
            return rootName;
        }         

        #endregion

        #region Public methods for Controller

        public string[] GetSchema()
        {
            IEnumerator<string> e = orderedElementNames.GetEnumerator();
            StringBuilder sb = new StringBuilder();
            StringWriter sw = new StringWriter(sb);

            while (e.MoveNext())
            {
                string elementname = e.Current;
                ElementDetails ed = elementList[elementname];
                SortedDictionary<string, ChildDetails> children = ed.children;
                SortedDictionary<string, ChildDetails>.KeyCollection childKeys = children.Keys;

                //EMPTY content
                if (childKeys.Count == 0 && !ed.hasCharacterContent)
                    sw.Write("<!ELEMENT " + elementname + " EMPTY>\n");

                //CHARACTER content
                if (childKeys.Count == 0 && ed.hasCharacterContent)
                    sw.Write("<!ELEMENT " + elementname + " (#PCDATA)>\n");

                //ELEMENT content
                if (childKeys.Count > 0 && !ed.hasCharacterContent)
                {
                    sw.Write("<!ELEMENT " + elementname + " (");

                    if (ed.sequenced)
                    {
                        IEnumerator<ChildDetails> c = ed.childSeq.GetEnumerator();
                        c.MoveNext();
                        while (true)
                        {
                            ChildDetails ch = c.Current;
                            sw.Write(ch.name);

                            if (ch.repeatable && !ch.optional)
                                sw.Write("+");
                            else if (ch.repeatable && ch.optional)
                                sw.Write("*");
                            else if (!ch.repeatable && ch.optional)
                                sw.Write("?");

                            if (c.MoveNext())
                                sw.Write(", ");
                            else
                                break;
                        }
                        sw.Write(")>\n");
                    }
                    else
                    {
                        IEnumerator c1 = childKeys.GetEnumerator();
                        while (c1.MoveNext())
                        {
                            sw.Write((string)c1.Current);

                            if (c1.MoveNext())
                                sw.Write("|");
                        }
                        sw.Write(")*>\n");
                    }
                }

                //MIXED content
                if (childKeys.Count > 0 && ed.hasCharacterContent)
                {
                    sw.Write("<!ELEMENT " + elementname + " (#PCDATA");
                    IEnumerator c2 = childKeys.GetEnumerator();

                    while (c2.MoveNext())
                    {
                        sw.Write("|" + (string)c2.Current);
                    }

                    sw.Write(")*>\n");
                }

                //Examine attributes
                SortedDictionary<string, AttributeDetails> attList = ed.attributes;
                bool doneID = false; //Ensure we have at most one ID attribute per element
                IEnumerator<string> a = attList.Keys.GetEnumerator();

                while (a.MoveNext())
                {
                    string attName = a.Current;
                    AttributeDetails ad = attList[attName];

                    bool required = (ad.occurrences == ed.occurrences); //If the attribute is present on every instance of the element, mark it as required
                    bool isId = ad.allNames && (!doneID) && (ad.unique) && (ad.occurrences >= minIdValues); //If every Value of the attribute is distinct and there are at least MIN_ID_VALUES, treat it as an ID
                    bool isFixed = required && ad.values.Count == 1 && ad.occurrences >= minFixed; //If there is only one attribute Value, and at least MIN_FIXED occurrences of it, mark it as FIXED
                    bool isEnum = ad.allNMTOKENs && (ad.occurrences >= minEnumInstances) && (ad.values.Count <= ad.occurrences / minEnumRatio) && (ad.values.Count <= maxEnumValues); //If the number of distinct values is small compared to the number of occurrences, treat it as an enumeration

                    sw.Write("<!ATTLIST " + elementname + " " + attName + " ");
                    string tokentype = (ad.allNMTOKENs ? "NMTOKEN" : "CDATA");

                    if (isId)
                    {
                        sw.Write("ID");
                        doneID = true;
                    }
                    else if (isFixed)
                    {
                        string val = (string)ad.values.First();
                        sw.Write(tokentype + " #FIXED \"" + Escape(val) + "\">\n");
                    }
                    else if (isEnum)
                    {
                        sw.Write("(");
                        IEnumerator v = ad.values.GetEnumerator();
                        v.MoveNext();

                        while (true)
                        {
                            sw.Write((string)v.Current);

                            if (!v.MoveNext())
                                break;

                            sw.Write("|");
                        }
                        sw.Write(")");
                    }
                    else
                        sw.Write(tokentype);

                    if (!isFixed)
                    {
                        if (required)
                            sw.Write(" #REQUIRED>\n");
                        else
                            sw.Write(" #IMPLIED>\n");
                    }
                }
            }

            return sb.ToString().TrimEnd('\r', '\n').Split(new char[] { '\n' });
        }

        public string[] GetMessage()
        {
            return null;
        }

        #endregion

        private void Parse(string url)
        {
            try
            {
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.DtdProcessing = DtdProcessing.Ignore;
                settings.IgnoreWhitespace = true;
                settings.IgnoreComments = true;

                XmlReader reader = XmlReader.Create(url, settings);
                bool rootFound = false;
                while (reader.Read())
                {
                    switch (reader.NodeType)
                    {
                        case XmlNodeType.Element:
                            bool isEmpty = reader.IsEmptyElement;
                            string namespaceURI = reader.NamespaceURI;
                            string name = reader.Name;

                            if (!rootFound)
                            {
                                rootName = name;
                                rootFound = true;
                            }

                            Dictionary<string, string> attributes = new Dictionary<string, string>();
                            if (reader.HasAttributes)
                            {
                                for (int i = 0; i < reader.AttributeCount; i++)
                                {
                                    reader.MoveToAttribute(i);
                                    attributes.Add(reader.Name, reader.Value);
                                }
                            }
                            StartElement(namespaceURI, name, name, attributes);

                            if (isEmpty)
                                EndElement(namespaceURI, name, name);
                            break;
                        case XmlNodeType.EndElement:
                            EndElement(reader.NamespaceURI, reader.Name, reader.Name);
                            break;
                        case XmlNodeType.Text:
                            Characters(reader.Value.ToCharArray(), 0, reader.Value.Length);
                            break;
                    }
                }
                reader.Close();
            }
            catch (XmlException e)
            {
                Console.WriteLine(e.Message);
            }
        }

        private bool IsValidName(string s)
        {
            if (!IsValidNMTOKEN(s))
                return false;

            try
            {
                XmlConvert.VerifyName(s);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        private bool IsValidNMTOKEN(string s)
        {
            if (String.IsNullOrEmpty(s))
                return false;

            try
            {
                XmlConvert.VerifyNMTOKEN(s);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        private static int Escape(char[] ch, int start, int length, char[] output)
        {
            int o = 0;

            for (int i = start; i < start + length; i++)
            {
                if (ch[i] == '<')
                {
                    ("&lt;").CopyTo(0, output, o, 4);
                    o += 4;
                }
                else if (ch[i] == '>')
                {
                    ("&gt;").CopyTo(0, output, o, 4);
                    o += 4;
                }
                else if (ch[i] == '&')
                {
                    ("&amp;").CopyTo(0, output, o, 5);
                    o += 5;
                }
                else if (ch[i] == '\"')
                {
                    ("&#34;").CopyTo(0, output, o, 5);
                    o += 5;
                }
                else if (ch[i] == '\'')
                {
                    ("&#39;").CopyTo(0, output, o, 5);
                    o += 5;
                }
                else if (ch[i] <= 0x7f)
                {
                    output[o++] = ch[i];
                }
                else
                {
                    string dec = "&#" + Convert.ToString((int)ch[i]) + ';';
                    dec.CopyTo(0, output, o, dec.Length);
                    o += dec.Length;
                }
            }
            return o;
        }

        private static string Escape(string input)
        {
            char[] dest = new char[input.Length * 8];
            int newlen = Escape(input.ToCharArray(), 0, input.Length, dest);
            return new String(dest, 0, newlen);
        }

        private void StartElement(string uri, string localName, string name, Dictionary<string, string> attributes)
        {
            StackEntry se = new StackEntry();    
            ElementDetails ed;
            ElementDetails edValue;

            if (elementList.TryGetValue(name, out edValue))
                ed = edValue;
            else
            {
                ed = new ElementDetails(name);
                elementList.Add(name, ed);
                orderedElementNames.Add(name);
            }

            se.elementDetails = ed;
            se.sequenceNumber = -1; //Initialize sequence numbering of child element types
            ed.occurrences++; //Increment occurrences of this element type

            // Handle the attributes accumulated for this element.
            IEnumerator<string> a = attributes.Keys.GetEnumerator();
            while (a.MoveNext())
            {
                string attName = a.Current;
                string val = attributes[attName];

                AttributeDetails ad;
                AttributeDetails adValue;

                if (ed.attributes.TryGetValue(attName, out adValue))
                {
                    ad = adValue;
                }
                else
                {
                    ad = new AttributeDetails(attName);
                    ed.attributes.Add(attName, ad);
                }

                if (!ad.values.Contains(val)) //New attribute Value
                { 
                    //ad.values.Add(val);

                    if (ad.allNames && !IsValidName(val)) //Check if attribute is a valid name
                        ad.allNames = false;

                    if (ad.allNMTOKENs && !IsValidNMTOKEN(val)) //Check if attribute is a valid NMTOKEN
                        ad.allNMTOKENs = false;

                    // Don't add new Value unless needed
                    if (ad.unique && ad.allNames && ad.occurrences <= MAX_ID_VALUES) //Looking for ID values
                        ad.values.Add(val);
                    else if (ad.values.Count <= maxEnumValues) //Looking for enumeration values
                        ad.values.Add(val);
                }
                else
                    ad.unique = false; //We have seen this attribute before

                ad.occurrences++;
            }

            //Keep track of the nesting and sequencing of child elements
            if (elementStack.Count != 0)
            {
                StackEntry parent = elementStack.Peek();
                ElementDetails parentDetails = parent.elementDetails;
                int seq = parent.sequenceNumber;

                //For sequencing, we're interested in consecutive groups of the same child element type
                bool isFirstInGroup = (parent.latestChild == null || (!parent.latestChild.Equals(name)));
                if (isFirstInGroup)
                {
                    seq++;
                    parent.sequenceNumber++;
                }
                parent.latestChild = name;
                
                SortedDictionary<string, ChildDetails> children = parentDetails.children;
                ChildDetails c;
                ChildDetails cValue;

                if (children.TryGetValue(name, out cValue)) //If we've seen this child of this parent before, get the details
                {
                    c = cValue;
                    //If it's the first Occurrence of the parent element and we've seen this child before, and it's the first of a new group, then the child occurrences are not consecutive
                    if (parentDetails.occurrences == 1 && isFirstInGroup)
                        parentDetails.sequenced = false;

                    //Check whether the position of this group of children in this parent element is the same as its position in previous instances of the parent.
                    if (parentDetails.childSeq.Count <= seq || !(parentDetails.childSeq[seq]).name.Equals(name))
                        parentDetails.sequenced = false;
                }
                else //First time seeing this child under this parent
                {
                    c = new ChildDetails();
                    c.name = name;
                    c.position = seq;
                    c.repeatable = false;
                    c.optional = false;
                    children.Add(name, c);
                    parentDetails.childSeq.Add(c);

                    if (parentDetails.occurrences != 1) //If this child is not on the first instance of the parent, mark it as optional
                        c.optional = true;
                }

                if (!isFirstInGroup) //If more than one child element, mark it as repeatable
                    c.repeatable = true;
            }
            elementStack.Push(se);
        }

        private void EndElement(string uri, string localName, string name)
        {
            ElementDetails ed = elementList[name];
            if (ed.sequenced)
            {
                StackEntry se = elementStack.Peek();
                int seq = se.sequenceNumber;
                for (int i = seq + 1; i < ed.childSeq.Count; i++)
                {
                    (ed.childSeq[i]).optional = true;
                }
            }
            elementStack.Pop();
        }

        private void Characters(char[] ch, int start, int length)
        {
            ElementDetails ed = (elementStack.Peek()).elementDetails;
            if (!ed.hasCharacterContent)
            {
                for (int i = start; i < start + length; i++)
                {
                    if ((int)ch[i] > 0x20)
                    {
                        ed.hasCharacterContent = true;
                        break;
                    }
                }
            }
        }
    }
}